﻿using System;
using System.ComponentModel;
using System.Globalization;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;

namespace iStudio.Structure.Control.Audio.TimeSelector
{
    /// <summary>
    /// A control that allows the user to
    /// see and edit TimeSpans.
    /// </summary>
    [DisplayName ("Time Editor"), Description ("Displays and edits time spans."), ToolboxItem (true), TemplatePart (Name = "PART_HoursTextBox", Type = typeof (TextBox)), TemplatePart (Name = "PART_MinutesTextBox", Type = typeof (TextBox)), TemplatePart (Name = "PART_SecondsTextBox", Type = typeof (TextBox)), TemplatePart (Name = "PART_SpinUpButton", Type = typeof (Button)), TemplatePart (Name = "PART_SpinDownButton", Type = typeof (Button))]
    
    public class TimeSelector : System.Windows.Controls.Control
    {
        #region  Fields

        private TimeEditFields _activeField = TimeEditFields.Seconds;
        private bool _hoursLoaded;
        private string _hoursMask;
        private TextBox _hoursTextBox;
        private bool _minutesLoaded;
        private string _minutesMask;
        private TextBox _minutesTextBox;
        private bool _secondsLoaded;
        private string _secondsMask;
        private TextBox _secondsTextBox;
        private ButtonBase _spinDownButton;
        private ButtonBase _spinUpButton;
        private bool _updatingValue;

        #endregion

        #region Enums

        private enum TimeEditFields
        {
            Hours,
            Minutes,
            Seconds
        }

        #endregion

        #region Constructors

        static TimeSelector ()
        {
            DefaultStyleKeyProperty.OverrideMetadata (typeof (TimeSelector), new FrameworkPropertyMetadata (typeof (TimeSelector)));
        }

        /// <summary>
        /// Creates an instance of TimeSelector
        /// </summary>
        public TimeSelector ()
        {
            UpdateMasks ();
        }

        #endregion

        #region Template Overrides

        /// <summary>
        /// When overridden in a derived class, is invoked whenever application code
        /// or internal processes call System.Windows.FrameworkElement.ApplyTemplate().
        /// </summary>
        public override void OnApplyTemplate ()
        {
            base.OnApplyTemplate ();

            _hoursTextBox = GetTemplateChild ("PART_HoursTextBox") as TextBox;
            _minutesTextBox = GetTemplateChild ("PART_MinutesTextBox") as TextBox;
            _secondsTextBox = GetTemplateChild ("PART_SecondsTextBox") as TextBox;
            _spinUpButton = GetTemplateChild ("PART_SpinUpButton") as Button;
            _spinDownButton = GetTemplateChild ("PART_SpinDownButton") as Button;

            if (_hoursTextBox != null)
            {
                _hoursTextBox.PreviewTextInput += HoursPreviewTextInput;
                _hoursTextBox.LostFocus += HoursLostFocus;
                _hoursTextBox.PreviewKeyDown += HoursKeyDown;
                _hoursTextBox.TextChanged += HoursTextChanged;
                _hoursTextBox.GotFocus += HoursGotFocus;
            }
            if (_minutesTextBox != null)
            {
                _minutesTextBox.PreviewTextInput += MinutesPreviewTextInput;
                _minutesTextBox.LostFocus += MinutesLostFocus;
                _minutesTextBox.PreviewKeyDown += MinutesKeyDown;
                _minutesTextBox.TextChanged += MinutesTextChanged;
                _minutesTextBox.GotFocus += MinutesGotFocus;
            }
            if (_secondsTextBox != null)
            {
                _secondsTextBox.PreviewTextInput += SecondsPreviewTextInput;
                _secondsTextBox.LostFocus += SecondsLostFocus;
                _secondsTextBox.PreviewKeyDown += SecondsKeyDown;
                _secondsTextBox.TextChanged += SecondsTextChanged;
                _secondsTextBox.GotFocus += SecondsGotFocus;
            }

            if (_spinUpButton != null)
            {
                _spinUpButton.Focusable = false;
                _spinUpButton.Click += SpinUpClick;
            }
            if (_spinDownButton != null)
            {
                _spinDownButton.Focusable = false;
                _spinDownButton.Click += SpinDownClick;
            }

            CopyValuesToTextBoxes ();
            UpdateReadOnly ();
        }

        #endregion

        #region Dependency Properties

        #region IsReadOnly

        /// <summary>
        /// Identifies the <see cref="IsReadOnly" /> dependency property. 
        /// </summary>
        public static readonly DependencyProperty IsReadOnlyProperty = DependencyProperty.Register ("IsReadOnly", typeof (bool), typeof (TimeSelector), new UIPropertyMetadata (false, OnIsReadOnlyChanged, OnCoerceIsReadOnly));

        /// <summary>
        /// Gets or sets whether the TimeSelector is readonly
        /// </summary>
        public bool IsReadOnly
        {
            // IMPORTANT: To maintain parity between setting a property in XAML and procedural code, do not touch the getter and setter inside this dependency property!
            get
            {
                return (bool) GetValue (IsReadOnlyProperty);
            }
            set
            {
                SetValue (IsReadOnlyProperty, value);
            }
        }

        private static object OnCoerceIsReadOnly (DependencyObject o, object value)
        {
            TimeSelector timeSelector = o as TimeSelector;
            if (timeSelector != null)
            {
                return timeSelector.OnCoerceIsReadOnly ((bool) value);
            }
            return value;
        }

        private static void OnIsReadOnlyChanged (DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            TimeSelector timeSelector = o as TimeSelector;
            if (timeSelector != null)
            {
                timeSelector.OnIsReadOnlyChanged ((bool) e.OldValue, (bool) e.NewValue);
            }
        }

        /// <summary>
        /// Coerces the value of <see cref="IsReadOnly"/> when a new value is applied.
        /// </summary>
        /// <param name="value">The value that was set on <see cref="IsReadOnly"/></param>
        /// <returns>The adjusted value of <see cref="IsReadOnly"/></returns>
        protected virtual bool OnCoerceIsReadOnly (bool value)
        {
            return value;
        }

        /// <summary>
        /// Called after the <see cref="IsReadOnly"/> value has changed.
        /// </summary>
        /// <param name="oldValue">The previous value of <see cref="IsReadOnly"/></param>
        /// <param name="newValue">The new value of <see cref="IsReadOnly"/></param>
        protected virtual void OnIsReadOnlyChanged (bool oldValue, bool newValue)
        {
            UpdateReadOnly ();
        }

        #endregion

        #region Minimum

        /// <summary>
        /// Identifies the <see cref="Minimum" /> dependency property. 
        /// </summary>
        public static readonly DependencyProperty MinimumProperty = DependencyProperty.Register ("Minimum", typeof (TimeSpan), typeof (TimeSelector), new UIPropertyMetadata (TimeSpan.Zero, OnMinimumChanged, OnCoerceMinimum));

        /// <summary>
        /// Gets or sets the minimum value that is allowed by the TimeSelector
        /// </summary>
        public TimeSpan Minimum
        {
            // IMPORTANT: To maintain parity between setting a property in XAML and procedural code, do not touch the getter and setter inside this dependency property!
            get
            {
                return (TimeSpan) GetValue (MinimumProperty);
            }
            set
            {
                SetValue (MinimumProperty, value);
            }
        }

        private static object OnCoerceMinimum (DependencyObject o, object value)
        {
            TimeSelector timeSelector = o as TimeSelector;
            if (timeSelector != null)
            {
                return timeSelector.OnCoerceMinimum ((TimeSpan) value);
            }
            return value;
        }

        private static void OnMinimumChanged (DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            TimeSelector timeSelector = o as TimeSelector;
            if (timeSelector != null)
            {
                timeSelector.OnMinimumChanged ((TimeSpan) e.OldValue, (TimeSpan) e.NewValue);
            }
        }

        /// <summary>
        /// Coerces the value of <see cref="Minimum"/> when a new value is applied.
        /// </summary>
        /// <param name="value">The value that was set on <see cref="Minimum"/></param>
        /// <returns>The adjusted value of <see cref="Minimum"/></returns>
        protected virtual TimeSpan OnCoerceMinimum (TimeSpan value)
        {
            return value;
        }

        /// <summary>
        /// Called after the <see cref="Minimum"/> value has changed.
        /// </summary>
        /// <param name="oldValue">The previous value of <see cref="Minimum"/></param>
        /// <param name="newValue">The new value of <see cref="Minimum"/></param>
        protected virtual void OnMinimumChanged (TimeSpan oldValue, TimeSpan newValue)
        {
            CoerceValue (MaximumProperty);
            CoerceValue (ValueProperty);
            UpdateMasks ();
        }

        #endregion

        #region Maximum

        /// <summary>
        /// Identifies the <see cref="Maximum" /> dependency property. 
        /// </summary>
        public static readonly DependencyProperty MaximumProperty = DependencyProperty.Register ("Maximum", typeof (TimeSpan), typeof (TimeSelector), new UIPropertyMetadata (TimeSpan.MaxValue, OnMaximumChanged, OnCoerceMaximum));

        /// <summary>
        /// Gets or sets the maximum value that is allowed by the TimeSelector
        /// </summary>
        public TimeSpan Maximum
        {
            // IMPORTANT: To maintain parity between setting a property in XAML and procedural code, do not touch the getter and setter inside this dependency property!
            get
            {
                return (TimeSpan) GetValue (MaximumProperty);
            }
            set
            {
                SetValue (MaximumProperty, value);
            }
        }

        private static object OnCoerceMaximum (DependencyObject o, object value)
        {
            TimeSelector timeSelector = o as TimeSelector;
            if (timeSelector != null)
            {
                return timeSelector.OnCoerceMaximum ((TimeSpan) value);
            }
            return value;
        }

        private static void OnMaximumChanged (DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            TimeSelector timeSelector = o as TimeSelector;
            if (timeSelector != null)
            {
                timeSelector.OnMaximumChanged ((TimeSpan) e.OldValue, (TimeSpan) e.NewValue);
            }
        }

        /// <summary>
        /// Coerces the value of <see cref="Maximum"/> when a new value is applied.
        /// </summary>
        /// <param name="value">The value that was set on <see cref="Maximum"/></param>
        /// <returns>The adjusted value of <see cref="Maximum"/></returns>
        protected virtual TimeSpan OnCoerceMaximum (TimeSpan value)
        {
            if (value < Minimum)
            {
                return Minimum;
            }
            return value;
        }

        /// <summary>
        /// Called after the <see cref="Maximum"/> value has changed.
        /// </summary>
        /// <param name="oldValue">The previous value of <see cref="Maximum"/></param>
        /// <param name="newValue">The new value of <see cref="Maximum"/></param>
        protected virtual void OnMaximumChanged (TimeSpan oldValue, TimeSpan newValue)
        {
            UpdateMasks ();
            NeedsHour = (newValue >= TimeSpan.FromHours (1.0d));
        }

        #endregion

        #region Value

        /// <summary>
        /// Identifies the <see cref="Value" /> dependency property. 
        /// </summary>
        public static readonly DependencyProperty ValueProperty = DependencyProperty.Register ("Value", typeof (TimeSpan), typeof (TimeSelector), new UIPropertyMetadata (TimeSpan.Zero, OnValueChanged, OnCoerceValue));

        /// <summary>
        /// Gets or sets the value of the TimeSelector
        /// </summary>
        public TimeSpan Value
        {
            // IMPORTANT: To maintain parity between setting a property in XAML and procedural code, do not touch the getter and setter inside this dependency property!
            get
            {
                return (TimeSpan) GetValue (ValueProperty);
            }
            set
            {
                SetValue (ValueProperty, value);
            }
        }

        private static object OnCoerceValue (DependencyObject o, object value)
        {
            TimeSelector timeSelector = o as TimeSelector;
            if (timeSelector != null)
            {
                return timeSelector.OnCoerceValue ((TimeSpan) value);
            }
            return value;
        }

        private static void OnValueChanged (DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            TimeSelector timeSelector = o as TimeSelector;
            if (timeSelector != null)
            {
                timeSelector.OnValueChanged ((TimeSpan) e.OldValue, (TimeSpan) e.NewValue);
            }
        }

        /// <summary>
        /// Coerces the value of <see cref="Value"/> when a new value is applied.
        /// </summary>
        /// <param name="value">The value that was set on <see cref="Value"/></param>
        /// <returns>The adjusted value of <see cref="Value"/></returns>
        protected virtual TimeSpan OnCoerceValue (TimeSpan value)
        {
            if (value < Minimum)
            {
                return Minimum;
            }
            if (value > Maximum)
            {
                return Maximum;
            }
            return value;
        }

        /// <summary>
        /// Called after the <see cref="Value"/> value has changed.
        /// </summary>
        /// <param name="oldValue">The previous value of <see cref="Value"/></param>
        /// <param name="newValue">The new value of <see cref="Value"/></param>
        protected virtual void OnValueChanged (TimeSpan oldValue, TimeSpan newValue)
        {
            if (!_updatingValue) // Avoid reentrance
            {
                _updatingValue = true;
                Hours = newValue.Hours;
                Minutes = newValue.Minutes;
                Seconds = newValue.Seconds + (newValue.Milliseconds / 1000.0d);
                CopyValuesToTextBoxes ();
                _updatingValue = false;
            }
        }

        #endregion

        #region Hours

        /// <summary>
        /// Identifies the <see cref="Hours" /> dependency property. 
        /// </summary>
        public static readonly DependencyProperty HoursProperty = DependencyProperty.Register ("Hours", typeof (int), typeof (TimeSelector), new UIPropertyMetadata (0, OnHoursChanged, OnCoerceHours));

        /// <summary>
        /// Gets or sets just the hours portion of the <see cref="Value"/>
        /// </summary>
        public int Hours
        {
            // IMPORTANT: To maintain parity between setting a property in XAML and procedural code, do not touch the getter and setter inside this dependency property!
            get
            {
                return (int) GetValue (HoursProperty);
            }
            set
            {
                SetValue (HoursProperty, value);
            }
        }

        private static object OnCoerceHours (DependencyObject o, object value)
        {
            TimeSelector timeSelector = o as TimeSelector;
            if (timeSelector != null)
            {
                return timeSelector.OnCoerceHours ((int) value);
            }
            return value;
        }

        private static void OnHoursChanged (DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            TimeSelector timeSelector = o as TimeSelector;
            if (timeSelector != null)
            {
                timeSelector.OnHoursChanged ((int) e.OldValue, (int) e.NewValue);
            }
        }

        /// <summary>
        /// Coerces the value of <see cref="Hours"/> when a new value is applied.
        /// </summary>
        /// <param name="value">The value that was set on <see cref="Hours"/></param>
        /// <returns>The adjusted value of <see cref="Hours"/></returns>
        protected virtual int OnCoerceHours (int value)
        {
            return value;
        }

        /// <summary>
        /// Called after the <see cref="Hours"/> value has changed.
        /// </summary>
        /// <param name="oldValue">The previous value of <see cref="Hours"/></param>
        /// <param name="newValue">The new value of <see cref="Hours"/></param>
        protected virtual void OnHoursChanged (int oldValue, int newValue)
        {
            const int maxHours = 23;
            if (newValue > maxHours)
            {
                newValue = maxHours;
            }

            TimeSpan value = new TimeSpan (0, newValue, Minutes, (int) Seconds, ((int) (Seconds * 1000)) % 1000);
            Value = value;
        }

        #endregion

        #region Minutes

        /// <summary>
        /// Identifies the <see cref="Minutes" /> dependency property. 
        /// </summary>
        public static readonly DependencyProperty MinutesProperty = DependencyProperty.Register ("Minutes", typeof (int), typeof (TimeSelector), new UIPropertyMetadata (0, OnMinutesChanged, OnCoerceMinutes));

        /// <summary>
        /// Gets or sets just the hours portion of the <see cref="Minutes"/>
        /// </summary>
        public int Minutes
        {
            // IMPORTANT: To maintain parity between setting a property in XAML and procedural code, do not touch the getter and setter inside this dependency property!
            get
            {
                return (int) GetValue (MinutesProperty);
            }
            set
            {
                SetValue (MinutesProperty, value);
            }
        }

        private static object OnCoerceMinutes (DependencyObject o, object value)
        {
            TimeSelector timeSelector = o as TimeSelector;
            if (timeSelector != null)
            {
                return timeSelector.OnCoerceMinutes ((int) value);
            }
            return value;
        }

        private static void OnMinutesChanged (DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            TimeSelector timeSelector = o as TimeSelector;
            if (timeSelector != null)
            {
                timeSelector.OnMinutesChanged ((int) e.OldValue, (int) e.NewValue);
            }
        }

        /// <summary>
        /// Coerces the value of <see cref="Minutes"/> when a new value is applied.
        /// </summary>
        /// <param name="value">The value that was set on <see cref="Minutes"/></param>
        /// <returns>The adjusted value of <see cref="Minutes"/></returns>
        protected virtual int OnCoerceMinutes (int value)
        {
            return value;
        }

        /// <summary>
        /// Called after the <see cref="Minutes"/> value has changed.
        /// </summary>
        /// <param name="oldValue">The previous value of <see cref="Minutes"/></param>
        /// <param name="newValue">The new value of <see cref="Minutes"/></param>
        protected virtual void OnMinutesChanged (int oldValue, int newValue)
        {
            const int maxMinutes = 59;
            if (newValue > maxMinutes)
            {
                newValue = maxMinutes;
            }

            TimeSpan value = new TimeSpan (0, Hours, newValue, (int) Seconds, ((int) (Seconds * 1000)) % 1000);
            Value = value;
        }

        #endregion

        #region Seconds

        /// <summary>
        /// Identifies the <see cref="Seconds" /> dependency property. 
        /// </summary>
        public static readonly DependencyProperty SecondsProperty = DependencyProperty.Register ("Seconds", typeof (double), typeof (TimeSelector), new UIPropertyMetadata (0.0d, OnSecondsChanged, OnCoerceSeconds));

        /// <summary>
        /// Gets or sets just the seconds portion of the <see cref="Value"/>
        /// </summary>
        public double Seconds
        {
            // IMPORTANT: To maintain parity between setting a property in XAML and procedural code, do not touch the getter and setter inside this dependency property!
            get
            {
                return (double) GetValue (SecondsProperty);
            }
            set
            {
                SetValue (SecondsProperty, value);
            }
        }

        private static object OnCoerceSeconds (DependencyObject o, object value)
        {
            TimeSelector timeSelector = o as TimeSelector;
            if (timeSelector != null)
            {
                return timeSelector.OnCoerceSeconds ((double) value);
            }
            return value;
        }

        private static void OnSecondsChanged (DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            TimeSelector timeSelector = o as TimeSelector;
            if (timeSelector != null)
            {
                timeSelector.OnSecondsChanged ((double) e.OldValue, (double) e.NewValue);
            }
        }

        /// <summary>
        /// Coerces the value of <see cref="Seconds"/> when a new value is applied.
        /// </summary>
        /// <param name="value">The value that was set on <see cref="Seconds"/></param>
        /// <returns>The adjusted value of <see cref="Seconds"/></returns>
        protected virtual double OnCoerceSeconds (double value)
        {
            return value;
        }

        /// <summary>
        /// Called after the <see cref="Seconds"/> value has changed.
        /// </summary>
        /// <param name="oldValue">The previous value of <see cref="Seconds"/></param>
        /// <param name="newValue">The new value of <see cref="Seconds"/></param>
        protected virtual void OnSecondsChanged (double oldValue, double newValue)
        {
            const double maxSeconds = 60.0d;
            if (newValue > maxSeconds)
            {
                newValue = maxSeconds - 0.01;
            }

            TimeSpan value = new TimeSpan (0, Hours, Minutes, (int) newValue, ((int) (newValue * 1000)) % 1000);
            Value = value;
        }

        #endregion

        #region CaretBrush

        /// <summary>
        /// Identifies the <see cref="CaretBrush" /> dependency property. 
        /// </summary>
        public static readonly DependencyProperty CaretBrushProperty = DependencyProperty.Register ("CaretBrush", typeof (Brush), typeof (TimeSelector), new UIPropertyMetadata (null, OnCaretBrushChanged, OnCoerceCaretBrush));

        /// <summary>
        /// Gets or sets the brush used by the caret in text fields.
        /// </summary>
        public Brush CaretBrush
        {
            // IMPORTANT: To maintain parity between setting a property in XAML and procedural code, do not touch the getter and setter inside this dependency property!
            get
            {
                return (Brush) GetValue (CaretBrushProperty);
            }
            set
            {
                SetValue (CaretBrushProperty, value);
            }
        }

        private static object OnCoerceCaretBrush (DependencyObject o, object value)
        {
            TimeSelector timeSelector = o as TimeSelector;
            if (timeSelector != null)
            {
                return timeSelector.OnCoerceCaretBrush ((Brush) value);
            }
            return value;
        }

        private static void OnCaretBrushChanged (DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            TimeSelector timeSelector = o as TimeSelector;
            if (timeSelector != null)
            {
                timeSelector.OnCaretBrushChanged ((Brush) e.OldValue, (Brush) e.NewValue);
            }
        }

        /// <summary>
        /// Coerces the value of <see cref="CaretBrush"/> when a new value is applied.
        /// </summary>
        /// <param name="value">The value that was set on <see cref="CaretBrush"/></param>
        /// <returns>The adjusted value of <see cref="CaretBrush"/></returns>
        protected virtual Brush OnCoerceCaretBrush (Brush value)
        {
            return value;
        }

        /// <summary>
        /// Called after the <see cref="CaretBrush"/> value has changed.
        /// </summary>
        /// <param name="oldValue">The previous value of <see cref="CaretBrush"/></param>
        /// <param name="newValue">The new value of <see cref="CaretBrush"/></param>
        protected virtual void OnCaretBrushChanged (Brush oldValue, Brush newValue)
        {
        }

        #endregion

        #region NeedsHour

        /// <summary>
        /// Identifies the <see cref="NeedsHour" /> dependency property. 
        /// </summary>
        public static readonly DependencyProperty NeedsHourProperty = DependencyProperty.Register ("NeedsHour", typeof (bool), typeof (TimeSelector), new UIPropertyMetadata (false, OnNeedsHourChanged, OnCoerceNeedsHour));

        private const double Epsilon = 0.00001;

        /// <summary>
        /// Gets whether the <see cref="Maximum"/> value is greater than the 1 hour
        /// to indicate whether the hour field needs to be displayed.
        /// </summary>
        public bool NeedsHour
        {
            // IMPORTANT: To maintain parity between setting a property in XAML and procedural code, do not touch the getter and setter inside this dependency property!
            get
            {
                return (bool) GetValue (NeedsHourProperty);
            }
            protected set
            {
                SetValue (NeedsHourProperty, value);
            }
        }

        private static object OnCoerceNeedsHour (DependencyObject o, object value)
        {
            TimeSelector timeSelector = o as TimeSelector;
            if (timeSelector != null)
            {
                return timeSelector.OnCoerceNeedsHour ((bool) value);
            }
            return value;
        }

        private static void OnNeedsHourChanged (DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            TimeSelector timeSelector = o as TimeSelector;
            if (timeSelector != null)
            {
                timeSelector.OnNeedsHourChanged ((bool) e.OldValue, (bool) e.NewValue);
            }
        }

        /// <summary>
        /// Coerces the value of <see cref="NeedsHour"/> when a new value is applied.
        /// </summary>
        /// <param name="value">The value that was set on <see cref="NeedsHour"/></param>
        /// <returns>The adjusted value of <see cref="NeedsHour"/></returns>
        protected virtual bool OnCoerceNeedsHour (bool value)
        {
            return value;
        }

        /// <summary>
        /// Called after the <see cref="NeedsHour"/> value has changed.
        /// </summary>
        /// <param name="oldValue">The previous value of <see cref="NeedsHour"/></param>
        /// <param name="newValue">The new value of <see cref="NeedsHour"/></param>
        protected virtual void OnNeedsHourChanged (bool oldValue, bool newValue)
        {
        }

        #endregion

        #endregion

        #region Event Handlers

        #region Buttons

        private void SpinUpClick (object sender, RoutedEventArgs e)
        {
            switch (_activeField)
            {
                case TimeEditFields.Hours:
                    IncrementHours ();
                    break;
                case TimeEditFields.Minutes:
                    IncrementMinutes ();
                    break;
                case TimeEditFields.Seconds:
                    IncrementSeconds ();
                    break;
            }
        }

        private void SpinDownClick (object sender, RoutedEventArgs e)
        {
            switch (_activeField)
            {
                case TimeEditFields.Hours:
                    DecrementHours ();
                    break;
                case TimeEditFields.Minutes:
                    DecrementMinutes ();
                    break;
                case TimeEditFields.Seconds:
                    DecrementSeconds ();
                    break;
            }
        }

        #endregion

        #region Key Presses

        private void HoursKeyDown (object sender, KeyEventArgs e)
        {
            switch (e.Key)
            {
                case Key.Enter:
                case Key.Tab:
                    ValidateHours ();
                    break;
                case Key.Up:
                    IncrementHours ();
                    break;
                case Key.Down:
                    DecrementHours ();
                    break;
                case Key.Right:
                    if (_hoursTextBox.CaretIndex >= _hoursTextBox.Text.Length && _minutesTextBox != null)
                    {
                        _minutesTextBox.Focus ();
                        _minutesTextBox.CaretIndex = 0;
                        e.Handled = true;
                    }
                    break;
                case Key.Escape:
                    ValidateHours ();
                    RemoveFocusToTop ();
                    break;
            }
        }

        private void MinutesKeyDown (object sender, KeyEventArgs e)
        {
            switch (e.Key)
            {
                case Key.Enter:
                case Key.Tab:
                    ValidateMinutes ();
                    break;
                case Key.Up:
                    IncrementMinutes ();
                    break;
                case Key.Down:
                    DecrementMinutes ();
                    break;
                case Key.Right:
                    if (_minutesTextBox.CaretIndex >= _minutesTextBox.Text.Length && _secondsTextBox != null)
                    {
                        _secondsTextBox.Focus ();
                        _secondsTextBox.CaretIndex = 0;
                        e.Handled = true;
                    }
                    break;
                case Key.Left:
                    if (_minutesTextBox.CaretIndex <= 0 && _hoursTextBox != null && _hoursTextBox.IsVisible)
                    {
                        _hoursTextBox.Focus ();
                        _hoursTextBox.CaretIndex = _hoursTextBox.Text.Length;
                        e.Handled = true;
                    }
                    break;
                case Key.Escape:
                    ValidateMinutes ();
                    RemoveFocusToTop ();
                    break;
                    // Do nothing
            }
        }

        private void SecondsKeyDown (object sender, KeyEventArgs e)
        {
            switch (e.Key)
            {
                case Key.Enter:
                case Key.Tab:
                    ValidateSeconds ();
                    break;
                case Key.Up:
                    IncrementSeconds ();
                    break;
                case Key.Down:
                    DecrementSeconds ();
                    break;
                case Key.Left:
                    if (_secondsTextBox.CaretIndex <= 0 && _minutesTextBox != null)
                    {
                        _minutesTextBox.Focus ();
                        _minutesTextBox.CaretIndex = _minutesTextBox.Text.Length;
                        e.Handled = true;
                    }
                    break;
                case Key.Escape:
                    ValidateSeconds ();
                    RemoveFocusToTop ();
                    break;
                    // Do nothing
            }
        }

        #endregion

        #region Text Input

        private void HoursPreviewTextInput (object sender, TextCompositionEventArgs e)
        {
            string text = _hoursTextBox.Text.Remove (_hoursTextBox.SelectionStart, _hoursTextBox.SelectionLength);
            text = text.Insert (_hoursTextBox.SelectionStart, e.Text);
            e.Handled = !(Regex.IsMatch (text, _hoursMask));
            base.OnPreviewTextInput (e);
        }

        private void MinutesPreviewTextInput (object sender, TextCompositionEventArgs e)
        {
            string text = _minutesTextBox.Text.Remove (_minutesTextBox.SelectionStart, _minutesTextBox.SelectionLength);
            text = text.Insert (_minutesTextBox.SelectionStart, e.Text);
            e.Handled = !(Regex.IsMatch (text, _minutesMask));
            base.OnPreviewTextInput (e);
        }

        private void SecondsPreviewTextInput (object sender, TextCompositionEventArgs e)
        {
            string text = _secondsTextBox.Text.Remove (_secondsTextBox.SelectionStart, _secondsTextBox.SelectionLength);
            text = text.Insert (_secondsTextBox.SelectionStart, e.Text);
            e.Handled = !(Regex.IsMatch (text, _secondsMask));
            base.OnPreviewTextInput (e);
        }

        private void SecondsTextChanged (object sender, TextChangedEventArgs e)
        {
            // Workaround for text clearing during initialization in OnTextContainerChanged()
            if (string.IsNullOrEmpty (_secondsTextBox.Text) && !_secondsLoaded)
            {
                CopyValuesToTextBoxes ();
                _secondsLoaded = true;
            }
        }

        private void MinutesTextChanged (object sender, TextChangedEventArgs e)
        {
            // Workaround for text clearing during initialization in OnTextContainerChanged()
            if (string.IsNullOrEmpty (_minutesTextBox.Text) && !_minutesLoaded)
            {
                CopyValuesToTextBoxes ();
                _minutesLoaded = true;
            }
        }

        private void HoursTextChanged (object sender, TextChangedEventArgs e)
        {
            // Workaround for text clearing during initialization in OnTextContainerChanged()
            if (string.IsNullOrEmpty (_hoursTextBox.Text) && !_hoursLoaded)
            {
                CopyValuesToTextBoxes ();
                _hoursLoaded = true;
            }
        }

        #endregion

        #region Focus

        private void HoursLostFocus (object sender, RoutedEventArgs e)
        {
            ValidateHours ();
        }

        private void MinutesLostFocus (object sender, RoutedEventArgs e)
        {
            ValidateMinutes ();
        }

        private void SecondsLostFocus (object sender, RoutedEventArgs e)
        {
            ValidateSeconds ();
        }

        private void HoursGotFocus (object sender, RoutedEventArgs e)
        {
            _activeField = TimeEditFields.Hours;
        }

        private void MinutesGotFocus (object sender, RoutedEventArgs e)
        {
            _activeField = TimeEditFields.Minutes;
        }

        private void SecondsGotFocus (object sender, RoutedEventArgs e)
        {
            _activeField = TimeEditFields.Seconds;
        }

        /// <summary>
        /// Invoked whenever an unhandled System.Windows.UIElement.GotFocus event reaches
        /// this element in its route.
        /// </summary>
        /// <param name="e">The System.Windows.RoutedEventArgs that contains the event data.</param>
        protected override void OnGotFocus (RoutedEventArgs e)
        {
            switch (_activeField)
            {
                case TimeEditFields.Hours:
                    if (_hoursTextBox != null)
                    {
                        _hoursTextBox.Focus ();
                    }
                    break;
                case TimeEditFields.Minutes:
                    if (_minutesTextBox != null)
                    {
                        _minutesTextBox.Focus ();
                    }
                    break;
                case TimeEditFields.Seconds:
                    if (_secondsTextBox != null)
                    {
                        _secondsTextBox.Focus ();
                    }
                    break;
            }

            base.OnGotFocus (e);
        }

        #endregion

        #endregion

        #region Private Utility Methods

        private void IncrementHours ()
        {
            Hours += 1;
            ValidateHours ();
        }

        private void DecrementHours ()
        {
            Hours = Math.Max (0, Hours - 1);
            ValidateHours ();
        }

        private void IncrementMinutes ()
        {
            if (Minutes >= 59)
            {
                IncrementHours ();
                Minutes = 0;
            }
            else
            {
                Minutes = Math.Min (60, Minutes + 1);
            }
            ValidateMinutes ();
        }

        private void DecrementMinutes ()
        {
            if (Minutes == 0 && Hours > 0)
            {
                DecrementHours ();
                Minutes = 59;
            }
            else
            {
                Minutes = Math.Max (0, Minutes - 1);
            }
            ValidateMinutes ();
        }

        private void IncrementSeconds ()
        {
            if (Seconds >= 59)
            {
                IncrementMinutes ();
                Seconds = 0;
            }
            else
            {
                Seconds = Math.Min (60.0d, Seconds + 1.0d);
            }
            ValidateSeconds ();
        }

        private void DecrementSeconds ()
        {
            if (Math.Abs (Seconds - 0) < Epsilon && Minutes > 0)
            {
                DecrementMinutes ();
                Seconds = 59;
            }
            else
            {
                Seconds = Math.Max (0, Seconds - 1.0d);
            }
            ValidateSeconds ();
        }

        private void ValidateHours ()
        {
            if (_hoursTextBox == null)
            {
                return;
            }

            int hours;
            int.TryParse (_hoursTextBox.Text, out hours);
            Hours = hours;
            CopyValuesToTextBoxes ();
        }

        private void ValidateMinutes ()
        {
            if (_minutesTextBox == null)
            {
                return;
            }

            int minutes;
            int.TryParse (_minutesTextBox.Text, out minutes);
            Minutes = minutes;
            CopyValuesToTextBoxes ();
        }

        private void ValidateSeconds ()
        {
            if (_secondsTextBox == null)
            {
                return;
            }

            double seconds;
            double.TryParse (_secondsTextBox.Text, out seconds);
            Seconds = seconds;
            CopyValuesToTextBoxes ();
        }

        private void UpdateMasks ()
        {
            _hoursMask = CreateMask (0, Minimum.TotalHours, false);
            _minutesMask = CreateMask (0, Math.Min (60, Maximum.TotalMinutes), false);
            _secondsMask = CreateMask (0, Math.Min (60, Maximum.TotalSeconds), true);
            CoerceValue (ValueProperty);
        }

        private void UpdateReadOnly ()
        {
            if (_hoursTextBox != null)
            {
                _hoursTextBox.IsReadOnly = IsReadOnly;
            }
            if (_minutesTextBox != null)
            {
                _minutesTextBox.IsReadOnly = IsReadOnly;
            }
            if (_secondsTextBox != null)
            {
                _secondsTextBox.IsReadOnly = IsReadOnly;
            }
            if (_spinUpButton != null)
            {
                _spinUpButton.IsEnabled = !IsReadOnly;
            }
            if (_spinDownButton != null)
            {
                _spinDownButton.IsEnabled = !IsReadOnly;
            }
        }

        private static string CreateMask (double minValue, double maxValue, bool allowDecimals)
        {
            string maskString;
            bool isNegative = minValue < 0;
            string allowNegativeString = isNegative ? @"-?" : string.Empty;
            string allowDecimalString = allowDecimals ? @"(\.\d{0,2})?" : string.Empty;
            if (Math.Abs (maxValue - 0) < Epsilon && Math.Abs (minValue - 0) < Epsilon)
            {
                maskString = string.Format ("^{0}{1}{2}$", allowNegativeString, @"\d{0,2}?", allowDecimalString);
            }
            else
            {
                int minValueLength = isNegative ? ((int) minValue).ToString (CultureInfo.InvariantCulture).Length : ((int) minValue).ToString (CultureInfo.InvariantCulture).Length + 1;
                int numericLength = Math.Max (minValueLength, ((int) maxValue).ToString (CultureInfo.InvariantCulture).Length);
                maskString = string.Format ("^{0}{1}{2}{3}{4}$", allowNegativeString, @"\d{0,", numericLength, @"}?", allowDecimalString);
            }
            return maskString;
        }

        private void CopyValuesToTextBoxes ()
        {
            if (_hoursTextBox != null)
            {
                _hoursTextBox.Text = Hours.ToString ("00");
            }
            if (_minutesTextBox != null)
            {
                _minutesTextBox.Text = Minutes.ToString ("00");
            }
            if (_secondsTextBox != null)
            {
                _secondsTextBox.Text = Seconds.ToString ("00.##");
            }
        }

        private void RemoveFocusToTop ()
        {
            FrameworkElement parent = Parent as FrameworkElement;
            while (parent != null && !((IInputElement) parent).Focusable)
            {
                parent = (FrameworkElement) parent.Parent;
            }

            DependencyObject scope = FocusManager.GetFocusScope (this);
            FocusManager.SetFocusedElement (scope, parent);
        }

        #endregion
    }
}